package com.tongweb.web.util.scan;

import com.tongweb.juli.logging.Log;
import com.tongweb.juli.logging.LogFactory;
import com.tongweb.web.Jar;
import com.tongweb.web.JarScanFilter;
import com.tongweb.web.JarScanType;
import com.tongweb.web.JarScanner;
import com.tongweb.web.JarScannerCallback;
import com.tongweb.web.util.ExceptionUtils;
import com.tongweb.web.util.buf.UriUtil;
import com.tongweb.web.util.res.StringManager;

import java.io.File;
import java.io.IOException;
import java.net.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import javax.servlet.ServletContext;

public class StandardJarScanner implements JarScanner {
    public static final Log log = LogFactory.getLog(StandardJarScanner.class);

    public static final StringManager sm = StringManager.getManager("com.tongweb.web.util.scan");

    public static final Set<ClassLoader> CLASSLOADER_HIERARCHY;

    static {
        Set<ClassLoader> cls = new HashSet<>();
        ClassLoader cl = StandardJarScanner.class.getClassLoader();
        while (cl != null) {
            cls.add(cl);
            cl = cl.getParent();
        }
        CLASSLOADER_HIERARCHY = Collections.unmodifiableSet(cls);
    }

    public boolean scanClassPath = true;

    public boolean isScanClassPath() {
        return this.scanClassPath;
    }

    public void setScanClassPath(boolean scanClassPath) {
        this.scanClassPath = scanClassPath;
    }

    public boolean scanManifest = true;

    public boolean isScanManifest() {
        return this.scanManifest;
    }

    public void setScanManifest(boolean scanManifest) {
        this.scanManifest = scanManifest;
    }

    public boolean scanAllFiles = false;

    public boolean isScanAllFiles() {
        return this.scanAllFiles;
    }

    public void setScanAllFiles(boolean scanAllFiles) {
        this.scanAllFiles = scanAllFiles;
    }

    public boolean scanAllDirectories = true;

    public boolean isScanAllDirectories() {
        return this.scanAllDirectories;
    }

    public void setScanAllDirectories(boolean scanAllDirectories) {
        this.scanAllDirectories = scanAllDirectories;
    }

    public boolean scanBootstrapClassPath = false;

    public boolean isScanBootstrapClassPath() {
        return this.scanBootstrapClassPath;
    }

    public void setScanBootstrapClassPath(boolean scanBootstrapClassPath) {
        this.scanBootstrapClassPath = scanBootstrapClassPath;
    }

    public JarScanFilter jarScanFilter = new StandardJarScanFilter();

    public JarScanFilter getJarScanFilter() {
        return this.jarScanFilter;
    }

    public void setJarScanFilter(JarScanFilter jarScanFilter) {
        this.jarScanFilter = jarScanFilter;
    }

    public void scan(JarScanType scanType, ServletContext context, JarScannerCallback callback) {
        if (log.isTraceEnabled())
            log.trace(sm.getString("jarScan.webinflibStart"));
        Set<URL> processedURLs = new HashSet<>();
        Set<String> dirList = context.getResourcePaths("/WEB-INF/lib/");
        if (dirList != null) {
            Iterator<String> it = dirList.iterator();
            while (it.hasNext()) {
                String path = it.next();
                if (path.endsWith(".jar") && getJarScanFilter().check(scanType, path.substring(path.lastIndexOf('/') + 1))) {
                    if (log.isDebugEnabled())
                        log.debug(sm.getString("jarScan.webinflibJarScan", new Object[]{path}));
                    URL url = null;
                    try {
                        url = context.getResource(path);
                        processedURLs.add(url);
                        process(scanType, callback, url, path, true, null);
                    } catch (IOException e) {
                        log.warn(sm.getString("jarScan.webinflibFail", new Object[]{url}), e);
                    }
                    continue;
                }
                if (log.isTraceEnabled())
                    log.trace(sm.getString("jarScan.webinflibJarNoScan", new Object[]{path}));
            }
        }
        try {
            URL webInfURL = context.getResource("/WEB-INF/classes");
            if (webInfURL != null) {
                processedURLs.add(webInfURL);
                if (isScanAllDirectories()) {
                    URL url = context.getResource("/WEB-INF/classes/META-INF");
                    if (url != null)
                        try {
                            callback.scanWebInfClasses();
                        } catch (IOException e) {
                            log.warn(sm.getString("jarScan.webinfclassesFail"), e);
                        }
                }
            }
        } catch (MalformedURLException malformedURLException) {
        }
        if (isScanClassPath()) {
            if (log.isTraceEnabled())
                log.trace(sm.getString("jarScan.classloaderStart"));
            ClassLoader stopLoader = null;
            if (!isScanBootstrapClassPath())
                stopLoader = ClassLoader.getSystemClassLoader().getParent();
            ClassLoader classLoader = context.getClassLoader();
            boolean isWebapp = true;
            while (classLoader != null && classLoader != stopLoader) {
                if (classLoader instanceof URLClassLoader) {
                    if (isWebapp)
                        isWebapp = isWebappClassLoader(classLoader);
                    Deque<URL> classPathUrlsToProcess = new LinkedList<>();
                    classPathUrlsToProcess.addAll(Arrays.asList(((URLClassLoader) classLoader).getURLs()));
                    while (!classPathUrlsToProcess.isEmpty()) {
                        URL url = classPathUrlsToProcess.pop();
                        if (processedURLs.contains(url))
                            continue;
                        ClassPathEntry cpe = new ClassPathEntry(url);
                        if ((cpe.isJar() || scanType == JarScanType.PLUGGABILITY || isScanAllDirectories()) && getJarScanFilter().check(scanType, cpe.getName())) {
                            if (log.isDebugEnabled())
                                log.debug(sm.getString("jarScan.classloaderJarScan", new Object[]{url}));
                            try {
                                processedURLs.add(url);
                                process(scanType, callback, url, null, isWebapp, classPathUrlsToProcess);
                            } catch (IOException ioe) {
                                log.warn(sm.getString("jarScan.classloaderFail", new Object[]{url}), ioe);
                            }
                            continue;
                        }
                        if (log.isTraceEnabled())
                            log.trace(sm.getString("jarScan.classloaderJarNoScan", new Object[]{url}));
                    }
                }
                classLoader = classLoader.getParent();
            }
        }
    }

    public static boolean isWebappClassLoader(ClassLoader classLoader) {
        return !CLASSLOADER_HIERARCHY.contains(classLoader);
    }

    public void process(JarScanType scanType, JarScannerCallback callback, URL url, String webappPath, boolean isWebapp, Deque<URL> classPathUrlsToProcess) throws IOException {
        if (log.isTraceEnabled())
            log.trace(sm.getString("jarScan.jarUrlStart", new Object[]{url}));
        if ("jar".equals(url.getProtocol()) || url.getPath().endsWith(".jar")) {
            if (url.getPath().indexOf("si_lib") == -1) {
                try (Jar jar = newInstance(url)) {
                    if (isScanManifest())
                        processManifest(jar, isWebapp, classPathUrlsToProcess);
                    callback.scan(jar, webappPath, isWebapp);
                }
            }
        } else if ("file".equals(url.getProtocol())) {
            try {
                File f = new File(url.toURI());
                if (f.isFile() && isScanAllFiles()) {
                    URL jarURL = UriUtil.buildJarUrl(f);
                    try (Jar jar = newInstance(jarURL)) {
                        if (isScanManifest())
                            processManifest(jar, isWebapp, classPathUrlsToProcess);
                        callback.scan(jar, webappPath, isWebapp);
                    }
                } else if (f.isDirectory()) {
                    if (scanType == JarScanType.PLUGGABILITY) {
                        callback.scan(f, webappPath, isWebapp);
                    } else {
                        File metainf = new File(f.getAbsoluteFile() + File.separator + "META-INF");
                        if (metainf.isDirectory())
                            callback.scan(f, webappPath, isWebapp);
                    }
                }
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                IOException ioe = new IOException();
                ioe.initCause(t);
                throw ioe;
            }
        }
    }

    public static void processManifest(Jar jar, boolean isWebapp, Deque<URL> classPathUrlsToProcess) throws IOException {
        if (isWebapp || classPathUrlsToProcess == null)
            return;
        Manifest manifest = jar.getManifest();
        if (manifest != null) {
            Attributes attributes = manifest.getMainAttributes();
            String classPathAttribute = attributes.getValue("Class-Path");
            if (classPathAttribute == null)
                return;
            String[] classPathEntries = classPathAttribute.split(" ");
            for (String classPathEntry : classPathEntries) {
                classPathEntry = classPathEntry.trim();
                if (classPathEntry.length() != 0) {
                    URL classPathEntryURL = null, jarURL = jar.getJarFileURL();
                    try {
                        URI jarURI = jarURL.toURI();
                        URI classPathEntryURI = jarURI.resolve(classPathEntry);
                        classPathEntryURL = classPathEntryURI.toURL();
                    } catch (Exception e) {
                        if (log.isDebugEnabled())
                            log.debug(sm.getString("jarScan.invalidUri", new Object[]{jarURL}), e);
                    }
                    classPathUrlsToProcess.add(classPathEntryURL);
                }
            }
        }
    }

    /**
     * 加载jar 包
     *
     * @author wei.hu
     * @date 2021-11-26
     */
    public static Jar newInstance(URL url) throws IOException {
        String urlString = url.toString();
        if (urlString.startsWith("file:")) {
            if (urlString.endsWith("!/")) {
                return new JarFileUrlJar(url, true);
            }
            if (urlString.indexOf("si_lib") > 0) {
                return new UrlJar(url);
            }
            return (Jar) new JarFileUrlJar(url, false);
        } else if (urlString.startsWith("war:file:")) {
            URL jarUrl = UriUtil.warToJar(url);
            return new JarFileUrlNestedJar(jarUrl);
        } else {
            System.out.println(" newinstance url " + url.getPath());
            return (Jar) (urlString.startsWith("file:") ? new JarFileUrlJar(url, false) : new UrlJar(url));
        }
    }


    public static class ClassPathEntry {
        public boolean jar;
        public String name;

        public ClassPathEntry(URL url) {
            String path = url.getPath();
            int end = path.lastIndexOf(".jar");
            if (end != -1) {
                this.jar = true;
                int start = path.lastIndexOf('/', end);
                this.name = path.substring(start + 1, end + 4);
            } else {
                this.jar = false;
                if (path.endsWith("/"))
                    path = path.substring(0, path.length() - 1);
                int start = path.lastIndexOf('/');
                this.name = path.substring(start + 1);
            }
        }

        public boolean isJar() {
            return this.jar;
        }

        public String getName() {
            return this.name;
        }
    }
}
